/*
 * @Descripttion:
 * @LastEditors: chen.long
 * @LastEditTime: 2024-02-24 14:45:25
 */

import { error } from '../utils/logger';
import { loadErrorHandler } from '../types/index.type';
import {
  YBMICRO_TIPS_CSS_ERROR_REQUESTED,
  YBMICRO_TIPS_HTML_ERROR_REQUESTED,
  YBMICRO_TIPS_SCRIPT_ERROR_REQUESTED
} from '../constant';
import {
  ImportEntryOpts,
  ScriptObject,
  ScriptResultList,
  StyleObject,
  StyleResultList,
  htmlParseResult
} from '../types/template.type';
import {
  compose,
  defaultGetPublicPath,
  getCurUrl,
  getInlineCode
} from '../utils';
import processTpl, {
  genLinkReplaceSymbol,
  getInlineStyleReplaceSymbol
} from './template';
import YbMicro from './sandbox';

const isInlineCode = code => code.startsWith('<');

const defaultFetch = window.fetch.bind(window);

const styleCache = {};
const scriptCache = {};
const embedHTMLCache = {};

/**
 * @Author: chen.long
 * @Descripttion: 为以后自定义解析模版预留
 * @Param:
 * @param {*} tpl
 */
function defaultGetTemplate(tpl) {
  return tpl;
}

/**
 * @Author: chen.long
 * @Descripttion: 获取资源文件
 */
const fetchAssets = (
  src: string,
  cache: Object,
  cssFlag?: boolean,
  loadError?: loadErrorHandler
) =>
  cache[src] ||
  (cache[src] = defaultFetch(src)
    .then(response => {
      if (response.status >= 400) {
        cache[src] = null;
        if (cssFlag) {
          error(YBMICRO_TIPS_CSS_ERROR_REQUESTED, { src, response });
          loadError?.(src, new Error(YBMICRO_TIPS_CSS_ERROR_REQUESTED));
          return '';
        } else {
          error(YBMICRO_TIPS_SCRIPT_ERROR_REQUESTED, { src, response });
          loadError?.(src, new Error(YBMICRO_TIPS_SCRIPT_ERROR_REQUESTED));
          throw new Error(YBMICRO_TIPS_SCRIPT_ERROR_REQUESTED);
        }
      }
      return response.text();
    })
    .catch(e => {
      cache[src] = null;
      if (cssFlag) {
        error(YBMICRO_TIPS_CSS_ERROR_REQUESTED, src);
        loadError?.(src, e);
        return '';
      } else {
        error(YBMICRO_TIPS_SCRIPT_ERROR_REQUESTED, src);
        loadError?.(src, e);
        return '';
      }
    }));

/**
 * @Author: chen.long
 * @Descripttion: 预获取样式文件
 * @Param:
 */
export function getExternalStyleSheets(
  styles: StyleObject[],
  loadError: loadErrorHandler
): StyleResultList {
  return styles.map(({ src, content, ignore }) => {
    // 内联
    if (content) {
      return { src: '', contentPromise: Promise.resolve(content) };
    } else if (isInlineCode(src)) {
      // 处理内联样式
      return { src: '', contentPromise: Promise.resolve(getInlineCode(src)) };
    } else {
      // 处理外挂样式
      return {
        src,
        ignore,
        contentPromise: ignore
          ? Promise.resolve('')
          : fetchAssets(src, styleCache, true, loadError)
      };
    }
  });
}

/**
 * @Author: chen.long
 * @Descripttion: 预获取脚本文件
 */
export function getExternalScripts(
  scripts: ScriptObject[],
  loadError: loadErrorHandler
): ScriptResultList {
  // 在iframe内部请求模块文件
  return scripts.map(script => {
    const { src, async, defer, module } = script;
    let contentPromise = null;
    if ((async || defer) && src && !module) {
      // async
      contentPromise = new Promise((resolve, reject) =>
        requestIdleCallback(() =>
          fetchAssets(src, scriptCache, false, loadError).then(resolve, reject)
        )
      );
    } else if (module && src) {
      // module
      contentPromise = Promise.resolve('');
    } else if (!src) {
      // 内联
      contentPromise = Promise.resolve(script.content);
    } else {
      // 外挂
      contentPromise = fetchAssets(src, scriptCache, false, loadError);
    }
    if (module && !async) script.defer = true;
    return { ...script, contentPromise };
  });
}

/**
 * @Author: chen.long
 * @Descripttion: 请求处理html
 */
export default function importHTML(params: {
  url: string;
  opts: ImportEntryOpts;
}): Promise<htmlParseResult> {
  const { url, opts } = params;
  const { loadError } = opts;
  const htmlLoader = defaultGetTemplate;
  const getPublicPath = defaultGetPublicPath;

  const getHtmlParseResult = (url, htmlLoader) =>
    fetch(url)
      .then(response => {
        if (response.status >= 400) {
          error(YBMICRO_TIPS_HTML_ERROR_REQUESTED, { url, response });
          loadError?.(url, new Error(YBMICRO_TIPS_HTML_ERROR_REQUESTED));
          return '';
        }
        return response.text();
      })
      .catch(e => {
        embedHTMLCache[url] = null;
        loadError?.(url, e);
        return Promise.reject(e);
      })
      .then(html => {
        const assetPublicPath = getPublicPath(url);
        const { template, scripts, styles } = processTpl(
          htmlLoader(html),
          assetPublicPath
        );
        return {
          template: template,
          assetPublicPath,
          getExternalScripts: () => getExternalScripts(scripts, loadError),
          getExternalStyleSheets: () =>
            getExternalStyleSheets(styles, loadError)
        };
      });

  return (
    embedHTMLCache[url] ||
    (embedHTMLCache[url] = getHtmlParseResult(url, htmlLoader))
  );
}

/**
 * @Author: chen.long
 * @Descripttion: 处理css-loader
 * @Param:
 */
export async function processCssLoader(
  sandbox: YbMicro,
  template: string,
  getExternalStyleSheets: () => StyleResultList
): Promise<string> {
  const curUrl = getCurUrl(sandbox.proxyLocation);
  const composeCssLoader = compose(
    sandbox.plugins.map(plugin => plugin.cssLoader)
  );
  const processedCssList: StyleResultList = getExternalStyleSheets().map(
    ({ src, ignore, contentPromise }) => ({
      src,
      ignore,
      contentPromise: contentPromise.then(content =>
        composeCssLoader(content, src, curUrl)
      )
    })
  );
  const embedHTML = await getEmbedHTML(template, processedCssList);
  return embedHTML;
}

/**
 * @Author: chen.long
 * @Descripttion: 将外挂的css链接转换为内联样式，这样可以获得更好地性能
 */
async function getEmbedHTML(
  template,
  styleResultList: StyleResultList
): Promise<string> {
  let embedHTML = template;

  return Promise.all(
    styleResultList.map((styleResult, index) =>
      styleResult.contentPromise.then(content => {
        if (styleResult.src) {
          embedHTML = embedHTML.replace(
            genLinkReplaceSymbol(styleResult.src),
            styleResult.ignore
              ? `<link href="${styleResult.src}" rel="stylesheet" type="text/css">`
              : `<style>/* ${styleResult.src} */${content}</style>`
          );
        } else if (content) {
          embedHTML = embedHTML.replace(
            getInlineStyleReplaceSymbol(index),
            `<style>/* inline-style-${index} */${content}</style>`
          );
        }
      })
    )
  ).then(() => embedHTML);
}
